#include "settingsFunctions.hpp"
#include "macros.hpp"
#include "bi_calcBlocksize.hpp"
#include "bi_indexHelper.hpp"
#include "bc_staticValue.hpp"
#include "vtkExport.hpp"
#include "importFunctions.hpp"
#include "loggerFunctions.hpp"

#include "infiltration.hpp"
#include "drainage.hpp"

namespace hms
{


std::shared_ptr<log::logger> createLogger( Settings& s ){
	std::shared_ptr<log::logger> logger {
		log::createLogger( getLogSettings(s) )
	};

	if ( s.logLevel )
		log::setLevel( logger, log::Sink::console, s.logLevel.value() );

	if ( s.quiet )
		logger->set_level( log::Level::off );

	s.setLogger();

	return logger;
}


void setBlocksize( MeshVariant& meshVar, const SWESettings& sweSettings ){
	/* Can't set block size through the solver, so we do it here.
	 * This should be temporary, as blocksize may be more useful as 
	 * a compile-time constant and/or should become a solver property
	 * (but traversal functions currently do not get the solver as
	 * an argument.) */
	std::visit( [&](auto& mesh){
		using MeshType = remove_qualifiers<decltype(mesh)>;
		if constexpr (std::is_base_of_v<StructMeshBase<MeshType>, MeshType>)
			mesh.blocksize(sweSettings.blocksizeX);
	}, meshVar );
}

template<typename MeshType>
void setBlocksizes( [[maybe_unused]] SWESolver<MeshType>& solver ){
	if constexpr ( SWESolver<MeshType>::isOrthogonal ){
		SWESettings& settings { solver.settings() };
		if (
			 settings.useIntegratedBlockLoop &&
			!settings.setBlocksizesManually
		){
			Array2i meshSize { solver.mesh().derived().size() };
			auto set = [&](Index& size, Index meshsize_dim ){
				size = swe::calculateBlocksize(
					meshsize_dim - 2 * swe::IndexHelper::_Mesh::nOuterLayers,
					settings.blocksizeAdjustment,
					settings.blocksizeThreshold
				);
			};
			set( settings.blocksizeX, meshSize.x() );
			set( settings.blocksizeY, meshSize.y() );
		}
	}
}

void printTimeSettings( const Time& time, log::logger& logger ){
	static constexpr int maxWidth {26}, pad {2};
	fmt::memory_buffer out;
	auto setting = [&]( std::string_view desc, const auto& val ){
		fmt::format_to(std::back_inserter(out), "\n{0}{1:<{2}}{3}",
			indent(), desc, maxWidth + pad, val
		);
	};
	setting( "start time:", time.current() );
	setting( "end time:"  , time.end() );
	setting( "max. step:" , time.maxTimeStep() );

	setting( "write at time intervals:", time.writeAtTimeIntervals() );
	if ( time.writeAtTimeIntervals() )
		setting( "write time interval:", time.writeTimeInterval() );
	else
		setting( "write step interval:", time.writeStepInterval() );

	setting( "use explicit initial step:", time.useExplicitFirstStep() );
	if ( time.useExplicitFirstStep() )
		setting( "initial time step:", time.step() );
	
	logger.info( "Time settings:{}", fmt::to_string(out) );
}

template<typename MeshType>
void printSolverSettings( const SWESolver<MeshType>& solver ){
	using magic_enum::enum_name;
	using S = SWESolver<MeshType>;
	using SourceFieldName = typename S::SourceFieldName;
	using std::string_literals::operator""s;

	int maxWidth {22};
	fmt::memory_buffer out;

	auto printToBuffer = [&]( std::string setting, const auto& value ){
		fmt::format_to(std::back_inserter(out),
			"\n{0}{2:<{1}} {3}",
			indent(), maxWidth + 1, setting + ':', value
		);
	};
	for ( int i{0}; i<S::nSourceFields; ++i ){
		auto sfname { static_cast<SourceFieldName>(i) };
		printToBuffer(
			"use"s + std::string{ enum_name(sfname) },
			solver.useSourceField(sfname)
		);
	}

	const SWESettings& settings { solver.settings() };

	#define PRINTSETTING(SETTING) \
		printToBuffer( HMS_TO_STRING(SETTING), settings.SETTING )
	#define PRINTENUM(ENUM) \
		printToBuffer( HMS_TO_STRING(ENUM), enum_name( settings.ENUM ) )

	PRINTSETTING(useIntegratedBlockLoop);
	if ( settings.useIntegratedBlockLoop ){
		PRINTSETTING(setBlocksizesManually);
		if (!settings.setBlocksizesManually){
			PRINTENUM(blocksizeAdjustment);
			if (settings.blocksizeAdjustment == swe::RemAdjust::threshold)
				PRINTSETTING(blocksizeThreshold);
		}
		PRINTSETTING(blocksizeX);
		PRINTSETTING(blocksizeY);
	} else {
		PRINTSETTING(useMuscl);
		PRINTSETTING(useAtomicWrite);
		PRINTENUM(edgeTraversal);
	}
	PRINTSETTING(maxCourant);
	PRINTSETTING(useFriction);
	PRINTENUM(limiterFunction);

	solver.logger().info(
		"Solver settings:\n{}Type: {}"
		"{}",
		indent(),
		type_name<remove_qualifiers<decltype(solver)>>(),
		fmt::to_string(out)
	);

	#undef PRINTSETTING
	#undef PRINTENUM
}

namespace internal
{

template<Uniformity _isUniform, typename MeshType>
constexpr std::string_view uniformity( const ConstantField<_isUniform, MeshType>& ){
	return magic_enum::enum_name(_isUniform);
}

template<Uniformity _isUniform, typename MeshType>
constexpr std::string_view uniformity( const SourceField<_isUniform, MeshType>& ){
	return magic_enum::enum_name(_isUniform);
}

} // namespace internal


template<typename MeshType>
void printFieldProperties( const SWESolver<MeshType>& solver ){
	using S = SWESolver<MeshType>;
	using FName = typename S::FieldName;
	using CFName = typename S::ConstantFieldName;
	using SFName = typename S::SourceFieldName;
	using magic_enum::enum_name;

	decltype( std::string{}.size() )
		maxLenPatchname {0},
		maxLenCF {0},
		maxLenSF {0};
	for ( const BoundaryPatch& p : solver.mesh().boundaryPatches() ){
		maxLenPatchname = std::max( maxLenPatchname, p.name().size() );
	}
	for (int i{0}; i<S::nConstantFields; ++i){
		maxLenCF = std::max(maxLenCF, enum_name(static_cast<CFName>(i)).size() );
	}
	for (int i{0}; i<S::nSourceFields; ++i){
		maxLenSF = std::max(maxLenSF, enum_name(static_cast<SFName>(i)).size() );
	}
	/* use same length for constant and source fields */
	maxLenSF = maxLenCF = std::max( maxLenCF, maxLenSF );

	int pad {4};
	fmt::memory_buffer out;

	for ( int i{0}; i < S::nFields ; ++i ){
		auto name { static_cast<FName>(i) };
		const Field<MeshType>& f { solver.field(name) };
		fmt::format_to(std::back_inserter(out),
			"\n{}{}, boundary conditions per patch:",
			indent(), enum_name(name)
		);
		for ( const BoundaryPatch& p : solver.mesh().boundaryPatches() ){
			fmt::format_to(std::back_inserter(out),
				"\n{0}{1:<{2}}{3}",
				indent(2), p.name() + ':', maxLenPatchname + pad,
				[&]() -> std::string_view {
					try {
						return enum_name( f.boundaryCondition(p).type() );
					} catch ( const std::runtime_error& ){
						return "NOT SET!";
					}
				}()
			);
		}
	}
	solver.logger().info("Fields:{}", fmt::to_string(out));
	out.clear();

	for ( int i{0}; i < S::nConstantFields ; ++i ){
		auto name { static_cast<CFName>(i) };
		std::visit( [&](const auto& cf ){
			fmt::format_to(std::back_inserter(out),
				"\n{0}{1:<{2}}{3}",
				indent(), std::string{enum_name(name)} + ':',
				maxLenCF + pad,
				internal::uniformity(cf)
			);
		}, solver.constantField(name) );
	}
	solver.logger().info("Constant fields:{}", fmt::to_string(out));
	out.clear();

	for ( int i{0}; i < S::nSourceFields ; ++i ){
		auto name { static_cast<SFName>(i) };
		fmt::format_to(std::back_inserter(out),
			"\n{0}{1:<{2}}{3}",
			indent(), std::string{enum_name(name)} + ':',
			maxLenSF + pad,
			[&]() -> std::string {
				if ( solver.useSourceField(name) ){
					return std::visit( [&](const auto& sf ){
						return fmt::format( "{}, update method: {}",
							internal::uniformity(sf),
							enum_name( sf.updateMethod() )
						);
					}, solver.sourceField(name) );
				} else
					return "disabled";
			}()
		);
	}
	solver.logger().info("Source fields:{}", fmt::to_string(out));
}


SWESolverVariant createSolver(
	Settings& settings,
	const MeshVariant& meshVar,
	const Time& time,
	const xml::XMLDocument* domainDoc,
	const xml::XMLDocument* boundaryDoc
){
	using namespace vtk;
	SWESolverVariant solverVar;

	std::visit( [&](const auto& mesh){
		using MeshType = remove_qualifiers<decltype(mesh)>;

		auto& solver = solverVar.emplace<SWESolver<MeshType>>( mesh, time );

		mesh.logger().debug("Setting solver properties");
		solver.settings() = std::move( settings.swe() );
		setBlocksizes(solver);

		xml::XMLDocument constDoc, sourceDoc;
		readFields(
			settings, solver,
			domainDoc, boundaryDoc
		);

		setInfiltrationCallable(solver);
		setDrainageCallable(solver);

		solver.initialise();
		
	}, meshVar );

	return solverVar;
}

/**
 * @brief Set paths derived from case name, case dir, and constants
 * inside the Settings object 
 * 
 * @param s Settings object to set paths in
 */
void setDerivedPaths( Settings& s ){
	using namespace vtk;

	/* set by parser */
	assert( !s.caseDir().empty() );
	/* case name must also be available */
	assert( !s.caseName().empty() );

	setVtkFilePath(s);
	s.path(Settings::Path::bindingFile , getBindingFilePath(s) );
	s.path(Settings::Path::boundaryFile, getBoundaryPath      ( s.caseDir() ) );
	s.path(Settings::Path::constFields , getConstantFieldsPath( s.caseDir() ) );
	s.path(Settings::Path::sourceFields, getSourceFieldsPath  ( s.caseDir() ) );

	s.logger().debug(
		"Paths set to:"
		"\ncase dir:      {}"
		"\nVTK file:      {}"
		"\nBinding file:  {}"
		"\nBoundary file: {}"
		"\nConst. fields: {}"
		"\nSource fields: {}",
		s.caseDir().string(),
		s.vtkFilePath().string(),
		s.bindingFilePath().string(),
		s.boundaryPath().string(),
		s.constFieldsPath().string(),
		s.sourceFieldsPath().string()
	);
}

void printCaseSetup(
	const Settings& settings,
	const SWESolverVariant& solverVar
){
	std::visit( [&](const auto& solver){		
		log::logger& logger { solver.logger() };

		log::printSection(logger, "Case setup", log::Level::info);

		logger.info( "Case name: {}", bold( settings.caseName() ) );
		printTimeSettings(solver.time(), logger);
		solver.mesh().printInfo();
		printSolverSettings(solver);
		printFieldProperties(solver);
		
	}, solverVar );
}


#define INSTANTIATE_FOR_ALL_MESHTYPES(MeshType) \
template void printSolverSettings( const SWESolver<MeshType>& ); \
template void printFieldProperties( const SWESolver<MeshType>& ); \
template void setBlocksizes( SWESolver<MeshType>& );

#include "meshInstantiator.hpp"

} // namespace hms
